/*
 *
 *  * Unidata Platform
 *  * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 *  *
 *  * Commercial License
 *  * This version of Unidata Platform is licensed commercially and is the appropriate option for the vast majority of use cases.
 *  *
 *  * Please see the Unidata Licensing page at: https://unidata-platform.com/license/
 *  * For clarification or additional options, please contact: info@unidata-platform.com
 *  * -------
 *  * Disclaimer:
 *  * -------
 *  * THIS SOFTWARE IS DISTRIBUTED "AS-IS" WITHOUT ANY WARRANTIES, CONDITIONS AND
 *  * REPRESENTATIONS WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE
 *  * IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, MERCHANTABLE QUALITY,
 *  * FITNESS FOR A PARTICULAR PURPOSE, DURABILITY, NON-INFRINGEMENT, PERFORMANCE AND
 *  * THOSE ARISING BY STATUTE OR FROM CUSTOM OR USAGE OF TRADE OR COURSE OF DEALING.
 *
 */

package org.unidata.mdm.data.service.segments.records.draft;

import java.time.Instant;
import java.util.Objects;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.core.type.data.OperationType;
import org.unidata.mdm.core.type.model.EntityElement;
import org.unidata.mdm.core.type.timeline.Timeline;
import org.unidata.mdm.data.context.DeleteRequestContext;
import org.unidata.mdm.data.context.RestoreRecordRequestContext;
import org.unidata.mdm.data.context.UpsertRequestContext;
import org.unidata.mdm.data.exception.DataExceptionIds;
import org.unidata.mdm.data.module.DataModule;
import org.unidata.mdm.data.type.data.OriginRecord;
import org.unidata.mdm.data.type.draft.DataDraftConstants;
import org.unidata.mdm.data.type.draft.DataDraftOperation;
import org.unidata.mdm.data.type.draft.DataDraftParameters;
import org.unidata.mdm.data.type.keys.RecordKeys;
import org.unidata.mdm.draft.context.DraftUpsertContext;
import org.unidata.mdm.draft.dto.DraftUpsertResult;
import org.unidata.mdm.draft.exception.DraftProcessingException;
import org.unidata.mdm.draft.type.Draft;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.system.type.pipeline.Start;
import org.unidata.mdm.system.type.variables.Variables;

/**
 * @author Alexey Tsarapkin
 */
@Component(RecordDraftUpsertStartExecutor.SEGMENT_ID)
public class RecordDraftUpsertStartExecutor extends Start<DraftUpsertContext, DraftUpsertResult> {

    public static final String SEGMENT_ID = DataModule.MODULE_ID + "[RECORD_DRAFT_UPSERT_START]";

    private static final String SEGMENT_DESCRIPTION = DataModule.MODULE_ID + ".record.draft.upsert.start.description";

    public RecordDraftUpsertStartExecutor() {
        super(SEGMENT_ID, SEGMENT_DESCRIPTION, DraftUpsertContext.class, DraftUpsertResult.class);
    }
    /**
     * MMS.
     */
    @Autowired
    private MetaModelService metaModelService;

    @Override
    public void start(@Nonnull DraftUpsertContext ctx) {
        setup(ctx);
    }

    @Nullable
    @Override
    public String subject(DraftUpsertContext ctx) {

        setup(ctx);

        Draft draft = ctx.currentDraft();
        Variables variables = draft.getVariables();
        return Objects.nonNull(variables)
                ? variables.valueGet(DataDraftConstants.ENTITY_NAME)
                : ctx.getParameter(DataDraftParameters.ENTITY_NAME);
    }

    protected void setup(DraftUpsertContext ctx) {

        if (ctx.setUp()) {
            return;
        }

        // 1. Init variables for new drafts
        setupVariables(ctx);

        ctx.setUp(true);
    }

    protected void setupVariables(DraftUpsertContext ctx) {

        Draft draft = ctx.currentDraft();
        boolean reset = stateReset(ctx, draft);
        boolean setup = !draft.isExisting() || reset;

        // Exisitng draft has properties already set
        // Moreover, we create variables and state on the first
        // call with data, so wait for it
        if (!setup) {
            return;
        }

        Variables variables = null;

        // The very first run.
        if (!draft.isExisting()) {

            String entityName  = ctx.getParameter(DataDraftParameters.ENTITY_NAME);

            EntityElement el = metaModelService.instance(Descriptors.DATA)
                    .getElement(entityName);

            if (Objects.isNull(el) || (!el.isLookup() && !el.isRegister())) {
                throw new DraftProcessingException(
                        "Parameter [{}] is empty or denotes a non-existing/wrong type entity. Only registers/lookups are supported by the type 'record'",
                        DataExceptionIds.EX_DATA_RECORD_DRAFT_NO_ENTITY_NAME,
                        DataDraftParameters.ENTITY_NAME);
            }

            variables = new Variables().add(DataDraftConstants.ENTITY_NAME, entityName);
        }

        // First payload
        if (reset) {

            DataDraftOperation operation = ctx.getParameter(DataDraftParameters.DRAFT_OPERATION);
            Objects.requireNonNull(operation, "Draft operation must not be null.");

            switch (operation) {
            case DELETE_RECORD:
            case DELETE_PERIOD:
                variables = setupDelete(operation, ctx.getPayload());
                break;
            case RESTORE_RECORD:
            case RESTORE_PERIOD:
                variables = setupRestore(operation, ctx.getPayload());
                break;
            case UPSERT_DATA:
                variables = setupUpsert(operation, ctx.getPayload());
                break;
            default:
                throw new DraftProcessingException("Unsupported draft operation type [{}].",
                        DataExceptionIds.EX_DATA_RECORD_DRAFT_UNSUPPORTED_OPERATION, operation.name());
            }
        }

        draft.setVariables(variables);

        ctx.putToUserStorage(DataDraftConstants.DRAFT_STATE_RESET, reset);
    }

    protected Variables setupDelete(DataDraftOperation operation, DeleteRequestContext dCtx) {

        Instant from = null;
        Instant to = null;

        // Period delete will take user specified dates.
        if (dCtx.isInactivatePeriod()) {
            from = dCtx.getValidFrom() != null ? dCtx.getValidFrom().toInstant() : null;
            to = dCtx.getValidTo() != null ? dCtx.getValidTo().toInstant() : null;
        }

        RecordKeys keys = dCtx.keys();
        return new Variables()
            .add(DataDraftConstants.IS_NEW_RECORD, keys.isNew())
            .add(DataDraftConstants.ETALON_ID, keys.getEtalonKey().getId())
            .add(DataDraftConstants.ENTITY_NAME, keys.getEntityName())
            .add(DataDraftConstants.EXTERNAL_ID, keys.getOriginKey().getExternalId())
            .add(DataDraftConstants.SOURCE_SYSTEM, keys.getOriginKey().getSourceSystem())
            .add(DataDraftConstants.OPERATION_CODE, operation)
            .add(DataDraftConstants.VALID_FROM, from)
            .add(DataDraftConstants.VALID_TO, to)
            .add(DataDraftConstants.SHARD, keys.getShard())
            .add(DataDraftConstants.LSN, keys.getEtalonKey().getLsn())
            .add(DataDraftConstants.OPERATION_TYPE, Objects.isNull(dCtx.operationType()) ? OperationType.DIRECT : dCtx.operationType());
    }

    protected Variables setupUpsert(DataDraftOperation operation, UpsertRequestContext uCtx) {
        RecordKeys keys = uCtx.keys();
        return new Variables()
            .add(DataDraftConstants.IS_NEW_RECORD, keys.isNew())
            .add(DataDraftConstants.ETALON_ID, keys.getEtalonKey().getId())
            .add(DataDraftConstants.ENTITY_NAME, keys.getEntityName())
            .add(DataDraftConstants.EXTERNAL_ID, keys.getOriginKey().getExternalId())
            .add(DataDraftConstants.SOURCE_SYSTEM, keys.getOriginKey().getSourceSystem())
            .add(DataDraftConstants.OPERATION_CODE, operation)
            .add(DataDraftConstants.SHARD, keys.getShard())
            .add(DataDraftConstants.LSN, keys.isNew() ? -1 :keys.getEtalonKey().getLsn())
            .add(DataDraftConstants.OPERATION_TYPE, Objects.isNull(uCtx.operationType()) ? OperationType.DIRECT : uCtx.operationType());
    }

    protected Variables setupRestore(DataDraftOperation operation, RestoreRecordRequestContext rCtx) {

        Instant from = null;
        Instant to = null;

        // Period restore is expected to select exactly one period by either asOf or f/t boundary.
        if (rCtx.isPeriodRestore()) {
            Timeline<OriginRecord> t = rCtx.nextTimeline();
            from = t.first().getValidFrom() != null ? t.first().getValidFrom().toInstant() : null;
            to = t.first().getValidTo() != null ? t.first().getValidTo().toInstant() : null;
        }

        RecordKeys keys = rCtx.keys();
        return new Variables()
            .add(DataDraftConstants.IS_NEW_RECORD, keys.isNew())
            .add(DataDraftConstants.ETALON_ID, keys.getEtalonKey().getId())
            .add(DataDraftConstants.ENTITY_NAME, keys.getEntityName())
            .add(DataDraftConstants.EXTERNAL_ID, keys.getOriginKey().getExternalId())
            .add(DataDraftConstants.SOURCE_SYSTEM, keys.getOriginKey().getSourceSystem())
            .add(DataDraftConstants.OPERATION_CODE, operation)
            .add(DataDraftConstants.VALID_FROM, from)
            .add(DataDraftConstants.VALID_TO, to)
            .add(DataDraftConstants.SHARD, keys.getShard())
            .add(DataDraftConstants.LSN, keys.getEtalonKey().getLsn())
            .add(DataDraftConstants.OPERATION_TYPE, Objects.isNull(rCtx.operationType()) ? OperationType.DIRECT : rCtx.operationType());
    }

    private boolean stateReset(DraftUpsertContext ctx, Draft draft) {

        if (!ctx.hasPayload()) {
            return false;
        }

        DataDraftOperation current = Objects.isNull(draft.getVariables())
                ? null
                : draft.getVariables().valueGet(DataDraftConstants.OPERATION_CODE, DataDraftOperation.class);

        // Schedule state reset,
        // if current is null or current != next
        DataDraftOperation next = ctx.getParameter(DataDraftParameters.DRAFT_OPERATION);

        // All draft operations, supplying payload, must also supply operation code
        Objects.requireNonNull(next, "Draft operation must not be null.");

        return next != current;
    }
}
